﻿Public Class clsDataYSI
    Inherits clsData

    Private _br As IO.BinaryReader

    Const YSIErrorText As String = "An error occurred while trying to interpret the binary YSI data file. Please note that support for this format is relatively new and the file format is poorly documented. If you encounter problems, please contact the developer. Alternatively, use the YSI EcoWatch program to export information from the binary file to a text file which WRDB can more easily interpret."

    Enum enumFieldType
        Temp_C
        Temp_F
        Temp_K
        SpCond_ms_cm
        SpCond_us_cm
        Cond_ms_cm
        Cond_us_cm
        Resist_mOhm_cm
        Resist_kOhm_cm
        Resist_Ohm_cm
        TDS_g_L
        TDS_kg_L
        Sal_ppt
        DOSat_pct
        DO_mg_l
        pH_su
        pH_mV
        Orp_mV
        Site
    End Enum

    Dim FieldCode() As Byte = {1, 2, 3, 6, 7, 4, 5, 9, 8, 94, 10, 95, 12, 14, 15, 18, 17, 19, 117}
    Dim RawDataFields As New List(Of enumFieldType) 'item types are added in order they appear in header

    Public Overrides Sub Initialize(ByVal _Filename As String)
        Try
            MyBase.Initialize(_Filename)
            _br = New IO.BinaryReader(IO.File.Open(_Filename, IO.FileMode.Open, IO.FileAccess.Read))
            mDataType = enumDataType.YSI
            mFieldNames.Clear()
            mFieldNames.Add("Date_Time")
            RawDataFields.Clear()

            'read header; assume first byte will always be 41hex
            If _br.ReadByte() <> &H41 Then Throw New IO.IOException("Unexpected data found in YSI .DAT file for file signature.")

            _br.ReadBytes(110) 'read the rest of the header

            'read fields (first will be time which is not identified in header)
            Dim fldNum As Integer = 1

            'each field starts with 42hex
            Do
                If _br.ReadByte() <> &H42 Then Throw New IO.IOException("Unexpected data found in YSI .DAT file for field marker.")
                _br.ReadBytes(2) 'skip 2
                Dim fld As Byte = _br.ReadByte
                Dim fldType As Integer = Array.IndexOf(FieldCode, fld)
                RawDataFields.Add(fldType)
                Dim fldName As String = "Unknown"
                If fldType <> -1 Then fldName = [Enum].GetName(GetType(enumFieldType), fldType)
                fldName &= "(" & fld & ")"
                'mFieldNames.Add(fldName)
                _br.ReadBytes(11)
                fldNum += 1
            Loop Until _br.PeekChar = &H44

            'now add standard fields that are desired
            mFieldNames.AddRange(New String() {"Temp", "Cond", "SpCond", "DO", "pH", "Site", "Salinity", "TDS", "DOPctSat"})
        Catch ex As Exception
            _br.Close()
            ErrorMsg(YSIErrorText, ex)
        End Try
    End Sub

    Public Overrides Function Read() As Boolean
        Try
            If MyBase.Read() Then
                If _br.BaseStream.Position >= _br.BaseStream.Length - 1 Then _br.Close() : Return False
                'read next time step
                Dim RawData As New List(Of Single)

                'first field is always time (seconds since 1 March 1984)--32 bit unsigned integer
                mItems.Clear()
                If _br.ReadByte() <> &H44 Then Throw New IO.IOException("Unexpected data found in YSI .DAT file for record marker.")
                Dim numsec As UInteger = _br.ReadUInt32
                mItems.Add(New Date(1984, 3, 1).AddSeconds(numsec).ToString("MM/dd/yyyy HH:mm"))
                For i As Integer = 0 To RawDataFields.Count - 1
                    Dim v As Single = _br.ReadSingle.ToString
                    'mItems.Add(v)
                    RawData.Add(v)
                Next
                'add standard fields
                Dim Temp As Single = RawData(RawDataFields.IndexOf(enumFieldType.Temp_C))
                Dim DoPct As Single = RawData(RawDataFields.IndexOf(enumFieldType.DOSat_pct))
                Dim Cond As Single = RawData(RawDataFields.IndexOf(enumFieldType.Cond_ms_cm))
                mItems.Add(Temp)
                mItems.Add(Cond)
                mItems.Add(SpCond(Cond, Temp))
                mItems.Add(DoConc(DoPct, Temp, Salinity(Cond, Temp)))
                mItems.Add(RawData(RawDataFields.IndexOf((enumFieldType.pH_su))))
                mItems.Add(RawData(RawDataFields.IndexOf((enumFieldType.Site))))
                mItems.Add(Salinity(Cond, Temp))
                mItems.Add(TDS(SpCond(Cond, Temp)))
                mItems.Add(DoPct)
                Return True
            End If
        Catch ex As Exception
            _br.Close()
            ErrorMsg(YSIErrorText, ex)
            Return False
        End Try
    End Function

    Public Overrides Sub Rewind()
        MyBase.Rewind()
        _br.Close()
        Initialize(FileName)
    End Sub

    Public Overrides Function PercentComplete() As Double
        Return _br.BaseStream.Position * 100.0 / _br.BaseStream.Length
    End Function

    Public Overrides Sub Close()
        MyBase.Close()
        _br.Close()
    End Sub

    'conversion equations

    Private Function SpCond(ByVal Cond_mS_cm As Single, ByVal Temp_C As Single) As Single
        'Ref: email from YSI of 12/10/2010
        Return Cond_mS_cm * 1000 / (1 + 0.0191 * (Temp_C - 25))
    End Function

    Public Shared Function DoSat(ByVal Temp_C As Single, ByVal Salinity_ppt As Single) As Single
        'Ref: Sect3-1.pdf
        Dim Temp_K As Single = Temp_C + 273.15
        Dim Chloride As Single = Salinity_ppt / 1.80655
        Dim _DoSat As Single = Math.Exp(-139.34411 + 157570.1 / Temp_K - 66423080 / Temp_K ^ 2 + 12438000000 / Temp_K ^ 3 - 862194900000 / Temp_K ^ 4 -
                        Chloride * (0.031929 - 19.428 / Temp_K + 3867.3 / Temp_K ^ 2))
        Return _DoSat
    End Function

    Private Function DoConc(ByVal DoPctSat As Single, ByVal Temp_C As Single, ByVal Salinity_ppt As Single) As Single
        Dim _DoConc As Single = DoPctSat / 100 * DoSat(Temp_C, Salinity_ppt)
        Return _DoConc
    End Function

    Private Function Salinity(ByVal Cond_mS_cm As Single, ByVal Temp_C As Single, Optional ByVal Pressure_dBar As Single = 0) As Single
        'Ref: Salinity Conversion from Cond&Temp.xls provided by YSI
        Dim a() As Single = {0.008, -0.1692, 25.3851, 14.0941, -7.0261, 2.7081}
        Dim b() As Single = {0.0005, -0.0056, -0.0066, -0.0375, 0.0636, -0.0144}, k As Single = 0.0162
        Dim c() As Single = {0.6766097, 0.0200564, 0.0001104259, -0.00000069698, 0.0000000010031}
        Dim d() As Single = {0.03426, 0.0004464, 0.4215, -0.003107}
        Dim e() As Single = {0.000207, -0.0000000637, 0.000000000003989}

        Dim R As Single = Cond_mS_cm / 42.914
        Dim Rp As Single = 1 + (Pressure_dBar * (e(0) + e(1) * Pressure_dBar + e(2) * Pressure_dBar ^ 2) / (1 + d(0) * Temp_C + d(1) * Temp_C ^ 2 + (d(2) + d(3) * Temp_C) * R))
        Dim Rt1 As Single = c(0) + c(1) * Temp_C + c(2) * Temp_C ^ 2 + c(3) * Temp_C ^ 3 + c(4) * Temp_C ^ 4
        Dim Rt As Single = R / (Rp * Rt1)
        Dim DeltaS As Single = (Temp_C - 15) / (1 + k * (Temp_C - 15)) * (b(0) + b(1) * Rt ^ 0.5 + b(2) * Rt + b(3) * Rt ^ 1.5 + b(4) * Rt ^ 2 + b(5) * Rt ^ 2.5)
        Dim S As Single = a(0) + a(1) * Rt ^ 0.5 + a(2) * Rt + a(3) * Rt ^ 1.5 + a(4) * Rt ^ 2 + a(5) * Rt ^ 2.5 + DeltaS
        Return S
    End Function

    Private Function TDS(ByVal SpCond_mS_cm As Single) As Single
        Return SpCond_mS_cm * 0.65 'apparent multiplier used by YSI; awaiting confirmation
    End Function
End Class
